iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
AI Engineering

30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察系列 第 28 篇

Day 28|評測結果:v1 到 v2,AI 說得好聽不代表說得對

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

Day 27 把整條流程接成 CLI 和 endpoint,但成功回傳 JSON,不代表健康洞察說得對。ok 只表示通過現有檢查:引用可能不完整、資料不足時可能下了結論,也可能整篇都在免責,沒有回答問題。

Day 26 用九個測例比較 prompt-v1/v2,各跑三次,pass 分別是 8/27 與 16/27。不過,差距主要來自免責句的用詞、evidence.kind 標錯與一次截斷,還不能說 v2 的健康解讀比較正確。今天要重跑並擴大測例,拆開看:多通過了哪些檢查,又多了哪些錯?

這次沿用九個舊測例,再把合成情境換新 seed,新增十個,共 19 個;期望都在呼叫模型前固定。本輪只比 prompt-v1/v2,每版各跑三次,共 114 次呼叫。輸出上限改為 16,384 token,Day 26 在 8,192 上限下的結果另表對照;選例與設定細節放在 Hands-on。

Concept

1. 先說清楚評測的真值是什麼

合成情境記錄「我注入了什麼事件」,summary-v2 記錄「程式算出什麼、交給模型看什麼」。兩者可能不同:事件已注入,偏離分數卻沒有超過門檻。本篇評的是模型有沒有照可見資料回答,不能拿來證明它判對了人的生理狀態。

例如 S-008 注入了事件,summary 卻沒標旗,模型就不該說「程式已偵測到事件」。這條期望目前只寫在測例描述裡,還沒有對應的自動規則,仍需人工判讀。其他可檢查的期望則寫進測例檔,例如哪些指標不能下結論、哪些仍應回答。

舊九例多數已在先前實驗出現,prompt 與詞表也曾參照它們調整,適合拿來查舊錯是否重現。新測例則在看模型答案前固定期望;若看完答案才改規則,必須記錄版本,才能分清是模型改變,還是評分方式改變。

2. 四個品質問題要分開計數

Faithfulness:引用能否支持整句話。 一筆 claim 的數值、日期、方向與判定狀態,都要有資料支持。path 存在只表示找得到來源,還得確認它支持了句中的哪些資訊。ALCE 用 citation recall 看陳述是否獲引用完整支持,用 citation precision 看每筆引用是否真的支持陳述。本篇的 faithfulness 接近前者,但單位是模型列出的 claim,沒有沿用論文的 NLI 模型評分。即使一句話與 summary 一致,也不代表 summary 本身正確;這是可驗證性與真實性的差別(Liu et al.)。

Evidence coverage:指定的欄位有沒有被引用。 本篇檢查 must_cite 指定的欄位或其子欄位,衡量引用範圍。「每句都有 evidence」已是驗證的基本要求,不另當成品質分數;有引用也不等於整句有依據。分母尤其要小心:Day 26 的 53 份可評分回應全通過 must_cite,但其中 23 份的清單是空的,實際有指定欄位可查的只有 30 份。空清單的 pass 不能算成功引用,也不能把欄位命中率當成主張獲支持的比例。

Abstention:該保留判斷的指標要保留,能回答的仍要回答。 abstain_scope 裡的指標不可判定,就不能說它相對 baseline 偏高、偏低或「沒有偏離」;must_answer 裡可判定的指標,仍須回答。全部拒答不算好成績。目前規則只查引用位置與部分方向詞,句意是否越界仍可能需要人看。

Hallucination:指出哪句話缺乏支持。 例如憑空補數字、把不可判定說成正常,或直接推論病因。判讀時要保留原句、引用與 summary,沿用 Day 24 的「支持、部分支持、不支持、不可解讀」四類;「無法判定」則留給資料狀態。部分支持可能只是引用不完整,不能和不支持混算。禁語命中也不等於 hallucination:「不是診斷」同樣含有「診斷」,因此詞表結果只是複核線索。

3. 結果表要能追溯版本與分母

每份結果都要找得到測例、輸入、prompt 與檢查規則版本、輪次和失敗原因。截斷或 API 失敗沒有可評分內容,要與內容不合格分開;pass、needs_human、fail 也要分列,不能合成一個「正確率」。

規則能檢查格式、引用與部分用詞,整句是否獲支持仍需清楚的判準與可複核的判讀。Day 26 的人工確認是 0 列,沒有人工標註可驗證 LLM-as-judge,所以本輪不納入 judge。next_step 與 summary 標旗是否矛盾仍未裁決;evidence.kind 標錯則另外計數,但保留在 fail 裡,讓兩天的裁決維持同一口徑。若修規則,就升版,並分清舊紀錄重評與新模型呼叫。

檢查全過,也可能是檢查沒抓到錯。Day 26 的五項期望檢查沒有 fail,還需要用刻意改壞的輸出測試,例如換掉數字、拿掉不確定性,看規則能否發現。這輪尚未做這項對照。

Hands-on

Dataset:19 個測例,13 條序列

舊九個(S-001~S-009)沿用 Day 26,期望不動。新十個用同一組 Day 14 情境設定、只換 seed(2801–2806)產生六條新序列,再由 days/day28/scan_candidates.py 依規則挑解讀日。規則在掃描資料前寫好,只有「事件已注入但沒標旗」這一條(S-016 用)是第一次掃描後補的,仍在呼叫模型前固定。

測例 類別 情境 summary 的狀態 不能下結論 必須引用
S-010 normal sleep_debt_d28 三項都沒標旗 — minutesAsleep
S-011 normal sync_delay_d28 三項都沒標旗 — rmssd
S-012 normal drift_vs_event_d28 三項都沒標旗,睡眠 +2.78 接近門檻 — minutesAsleep
S-013 deviation sleep_debt_d28 resting_hr 標旗 up;rmssd 不可判定 rmssd resting_hr
S-014 deviation suspected_illness_d28 resting_hr 標旗 up — resting_hr
S-015 deviation conflicting_rhr_down_d28 resting_hr 標旗 down,rmssd −2.92 沒過門檻 — resting_hr
S-016 deviation drift_vs_event_d28 事件首日,但三項都沒標旗 — resting_hr
S-017 insufficient sleep_debt_d28 三項 baseline 都不足 三項 (空)
S-018 missing conflicting_rhr_down_d28 rmssd 當天缺值 rmssd minutesAsleep
S-019 missing mostly_missing_d28 一半的日子沒戴,三項都不可判定 三項 (空)

「三項」是 minutesAsleep、rmssd、resting_hr;「必須引用」指該指標的 deviation 欄位,解讀日與完整期望見 days/day28/results.md §2。新測例的必須引用用同一條規則:有標旗就引用標旗的指標,沒有就引用 |score| 最大的可判定指標。舊九個是 Day 26 逐例寫的,規則不同,所以後面的表都把舊、新分開。

有兩類沒補到。換 seed 後,原本設計成「兩項指標方向矛盾」的序列,rmssd 差一點沒過門檻(−2.92,門檻 3.0),60 天裡沒有一天是 conflicting;模擬同步延遲的序列也沒有誤報日。效果量貼著門檻時,有沒有標旗要看 seed。

期望先對照重新產生的 summary 逐一核對,commit 凍結(357353e)後才加入 runner,再開始呼叫模型。tests/test_day28.py 可以重驗期望與輸入一致,但這不證明期望本身寫對。

Method:兩個 prompt 版本,114 次呼叫

v1 與 v2 只差 prompt 的規則 9(src/wearable_ai/ai/prompts.py)。輸入、輸出格式、模型(gemini-3.8-flash,temperature 1.0)都和 Day 26 相同,唯一改的是輸出上限:Day 25、26 在 8,192 token 下都出現 thinking 吃完額度的截斷,這次提高到 16,384。

19 測例 × 2 版 × 3 輪 = 114 次。同一測例的 v1、v2 接著跑,先後順序每輪交替,避免某一版固定排前面。

評分沿用 evalcheck-v2,看完結果也沒有改。檢查分三層:有沒有完整回應 → 結構與引用契約(schema、evidence_exists、cross_field)→ 測例期望(must_cite、abstain_scope、must_answer、forbidden_phrases、max_claims),裁決取最嚴的一項。表格口徑在跑之前定好:每張表寫分母;evidence.kind 標錯另外計數,但仍算 fail;空的必須引用清單不算成功引用;舊、新測例分開,Day 26 的 54 次只另表對照。

這輪沒有人工判讀,沒有 judge,也還沒做「刻意改壞的輸出」對照。

Code

呼叫順序(days/day28/eval_run.py):

def plan(spec_ids, runs=RUNS, first_run=1):
    """每輪依序跑每個測例;同一測例的 v1、v2 相鄰,誰先依 (輪次 + 測例序號) 的奇偶交替。"""
    todo = []
    for k in range(first_run, first_run + runs):
        for i, sid in enumerate(spec_ids):
            order = VERSIONS if (k + i) % 2 == 0 else VERSIONS[::-1]
            todo += [(group_name(sid, v), k) for v in order]
    return todo

evalcheck 每份只列前 3 筆 kind 錯誤,所以計數改讀驗證器 grounding.validate_output 的完整清單(days/day28/results_tables.py 的 kind_errors)。重現:

uv run python days/day28/eval_run.py --section 1            # 組輸入、核對期望,不呼叫 API
uv run python days/day28/eval_run.py --section 2 --runs 3   # 114 次呼叫
uv run python days/day28/eval_run.py --section 3            # 評分,寫 results.csv
uv run python days/day28/results_tables.py                  # 重產 results.md

結果一:114 次都完整回應,但離上限不遠

114 次都是第一次就成功、沒有截斷,耗時中位數 v1 18.0 秒、v2 22.0 秒。不過最大的一筆輸出加 thinking 用了 16,020 token,是上限的 98%;有 5 筆超過 Day 26 的上限 8,192(days/day28/results.md §1,超過 8,192 的筆數由 runs/*.json 重算)。

結果二:v2 的 pass 和 fail 都比較多

cohort prompt 次數 pass needs_human fail
舊九個 prompt-v1 27 6 18 3
舊九個 prompt-v2 27 8 10 9
新十個 prompt-v1 30 12 12 6
新十個 prompt-v2 30 14 12 4
合計 prompt-v1 57 18 30 9
合計 prompt-v2 57 22 22 13

114 份都可評分,沒有截斷或 API 失敗。逐項拆開:

檢查 v1 pass/nh/fail/skipped v2 pass/nh/fail/skipped
schema、evidence_exists 57/0/0/0 57/0/0/0
cross_field 50/0/7/0 44/0/13/0
must_cite 57/0/0/0 57/0/0/0
abstain_scope 20/0/1/36 21/0/0/36
must_answer 45/0/0/12 45/0/0/12
forbidden_phrases 23/33/1/0 28/29/0/0
max_claims 57/0/0/0 57/0/0/0

22 個 fail 裡有 20 個是 cross_field,另外 2 個都在 v1(結果四)。v2 比 v1 多 4 個 pass,也多 4 個 fail;只看 pass 會漏掉後一半。

同一格(測例 × 版本)跑三次,結果也常不一樣。v2 只有 S-008 三次都 fail,v1 沒有這樣的格子(days/day28/results.md §4)。

結果三:同樣九個測例,v2 的 pass 減半,版本差距縮小

舊九個的輸入、prompt 與評分規則都和 Day 26 相同,差在呼叫日期與輸出上限(days/day28/results.md §9):

條件 prompt pass needs_human fail truncated kind 標錯(紀錄/筆)
Day 26(8,192) v1 8 13 5 1 5/13
Day 26(8,192) v2 16 8 3 0 3/7
Day 28(16,384) v1 6 18 3 0 3/9
Day 28(16,384) v2 8 10 9 0 9/21

每列 27 次。v2 的 pass 從 16 變 8,fail 從 3 變 9,多出來的都是 kind 標錯;v1 變化小得多。v2 的 pass 仍比 v1 多,但差距從 8 份縮成 2 份,fail 則從較少變成較多。上限和日期同時改變,每格又只有三次,這張表分不出原因。

結果四:fail 幾乎都是 kind 標錯,另外兩個是引用位置不符與詞表誤判

kind 標錯:20 份、48 筆。 引用的欄位都存在,錯在把它的類別標成 data_note(資料備註)(days/day28/results.md §5):

被引用的欄位 正確的 kind v1 筆數 v2 筆數
baseline.min_obs_for_center program_judgement 11 9
daily[n].missing_kind observation 3 14
cross_metric.category、pattern program_judgement 0 6
daily[n].date observation 3 0
deviation.reason program_judgement 2 0

前兩類 Day 26 也出現過。第一類出在資料不足的說明,例如 S-019:「僅有 6 筆觀測值……未達計算中心值所需的 14 筆」,同一句其他引用都標對,只有「需要 14 筆」這個門檻標成資料備註。第二類出在描述缺值日;v2 的 14 筆裡有 9 筆來自 S-008 的三輪,都是同一種句子,所以 v2 這類多,主要集中在一格。

abstain_scope 的 1 個 fail(S-017 v1)。 三項都要保留判斷,模型寫「所有指標目前皆無法判定」,句意沒錯,但引用只指到睡眠,沒有指到 rmssd 與 resting_hr。規則要求逐項引用,所以判 fail。

forbidden_phrases 的 1 個 fail(S-011 v1)。 句子是免責句「……非臨床診斷量測值」,但「非」不在否定詞表,命中「診斷」就判 fail。83 個禁語命中裡,其餘 82 個前面都有否定詞,送人工複核。這和 Day 26 的「缺乏」是同一種漏洞;評分規則這輪固定不改,這個 fail 照算。

結果五:指定欄位都有引用,逐句檢查大多要人看

排除 36 份空清單後,78 份回應都引用了指定欄位,舊、新測例都是 100%(days/day28/results.md §6)。和 Day 26 一樣,這項檢查沒有挑出問題。

Day 24 的逐句 checker(不計入裁決)把兩版各約 575 筆 claim 中約七成判為 needs_human(v1 401/574、v2 402/579,days/day28/results.md §8)。這輪沒有人看。

結果與意外

原本以為 實際發現
我原本以為,沿用同九個測例與評分規則重跑,v2 的 pass 數會接近 Day 26 的 16/27,對 v1 的優勢也會相近。 同樣九個測例、同樣 prompt 與檢查規則,v2 的 pass 從 Day 26 的 16/27 變成 8/27,fail 從 3 變 9;v2 對 v1 的 pass 差距從 8 份縮成 2 份,fail 由較少變成較多。
我原本以為,v2 的規則 9 把不可判定範圍說得更明確,會讓更多輸出通過,而不會同時增加硬性違規。 v2 合計比 v1 多 4 個 pass、也多 4 個 fail(22 vs 18、13 vs 9),少 8 個 needs_human。
我原本以為,新增的指標層級期望會比通用結構檢查更能挑出測例特有的錯;kind 標錯不會主導 fail。 22 個 fail 有 20 個是 kind 標錯(48 筆,全標成 data_note);測例指定的引用、拒答與必答檢查只抓到 1 個,而且是引用位置不符、句意沒錯;共用禁語詞表另有 1 個已知誤判。
我原本以為,Day 26 已補過「缺乏」這個否定詞,免責句被當成違規的問題在這輪應該較少出現。 唯一的 forbidden_phrases fail 是免責句「非臨床診斷量測值」,因為「非」不在否定詞表;和 Day 26 的「缺乏」同一種漏洞。
我原本以為,把輸出上限從 8,192 提到 16,384,足以消除截斷,也會留下充裕的餘量。 max_output_tokens 16384 下 114 次沒有截斷,但最大一筆用到上限的 98%,5 筆超過 Day 26 的上限 8,192。
我原本以為,排除空清單後,must_cite 至少會抓到一些漏引指定欄位的回應;新測例的選取規則也能提供辨別力。 必要欄位引用覆蓋率 78/78;舊、新兩套 must_cite 規則都沒挑出問題。

Limitations

這輪比較只有一個模型、固定的一句問題,19 個測例在每版各跑三次;這些次數可呈現本批輸出的波動,還不足以把 pass 數差異當成穩定的版本效果。evalcheck-v2 只檢查預設的結構、引用與部分期望,沒有判 next_step 是否與標旗矛盾(已知案例 C16.n1);也沒有用刻意改壞的輸出測試規則能抓到多少錯。逐句 checker 約七成 claim 是 needs_human,本輪人工確認為零,因此 78/78 的指定欄位引用與自動裁決都不能當成逐句忠實度或 hallucination 的人工驗證。詞表把「非臨床診斷量測值」判成 fail,說明裁決仍含已知的字面誤判。舊九個測例多數在 Day 20–26 已看過,prompt 與詞表曾參照它們調整;Day 26 與本輪又同時相隔兩天並把 max_output_tokens 從 8,192 提到 16,384,無法從兩張結果表分辨 v2 變化的原因。

資料是合成情境及其產生的 summary-v2,評測期望也依這份 summary 訂定;結果只表示輸出是否符合模型可見資料和現有檢查,不能推論真實穿戴量測、人的生理狀態或醫療判斷準確率。19 個測例來自 13 條序列,新六條雖換了 seed,仍沿用舊情境的生成設定,雜訊可獨立而產生過程不獨立,不能把 114 次呼叫視為 114 個獨立情境。事件效果量靠近偵測門檻時,換 seed 便可能改變 summary 的標旗:這批沒有第二個 contradictory 測例,sync_delay_d28 也沒有誤報日,因此相關邊界沒有被新測例覆蓋;S-016 的事件雖已注入,summary 未標旗,本篇只能檢查模型有沒有忠於未標旗的輸入。

這對 AI Engineering 的意義

第一,回歸結果要能重現才算結論。Day 26 的 v2 優勢,這次在同樣九個測例上只剩一小段差距;如果當時就據此選定 v2,等於把一次抽樣當成性質。temperature 1.0 下每格跑 3 次,只能記錄這批有沒有出現、出現幾次,不足以估計發生率或版本差異。要比較兩個版本,次數、條件與改了哪個變數都得先寫清楚,結果不一致時才知道該懷疑什麼。

第二,pass 變多不等於變好。pass、needs_human、fail 要一起報,再拆到逐項與逐格,才看得到 fail 也跟著變多。

第三,fail 多半來自共用的結構契約,不是測例專屬的期望。kind 標錯不需要 ground truth 就抓得到;測例期望目前主要檢查引用結構與有限的字面規則,抓不到「引用了但說錯」。要知道規則抓不抓得到錯,下一步是用刻意改壞的輸出當對照,而不是再加測例。

最後,凍結評分規則有代價,也有用處。「非」的漏洞是看到輸出才發現的,但這次沒有當場改詞表,兩天的裁決才是同一把尺,能直接對照。要修就升版、把兩天的紀錄都重評,差異另表列出。


上一篇
Day 27|Prototype:CLI 與單一 API endpoint,以及模型、成本與快取的取捨
下一篇
Day 29|Safety、Privacy 與醫療邊界:系統知道何時不能回答嗎?
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言